מיון (Sorting) void BubbleSort(int* A, int n){ for (i = ; i < n-; i++) for (j = n-; j >= i; j--) if ( a[j] > a[j+]) swap(&a[j], &a[j+]); מערך בן מספרים. קלט: מערך ובו המספרים מאוחסנים בסדר עולה (או יורד). פלט: שיטות מיון נאיביות דורשות זמן ).Θ(n לדוגמא, החלפת איברים סמוכים Sort) :(Bubble בכל שלב האיבר הקל ביותר שלא במקומו מבעבע למעלה הערה: בד"כ ממיינים רשומות לפי המפתח שלהם ולא סתם מספרים. לשם בחינת שיטות המיון מספיק לבחון את מיון המפתחות עצמם. כאשר הרשומות מכילות את ולא לרשומות משנה בכל שלב מצביעים אלגוריתם מיון אינפורמציה רבה, הרשומות עצמן. cs, Technion מיונים א': מיון ערימה () חומר קריאה לשיעור זה Chapter - Heapsort Chapter - Quicksort Lecture of Geiger & Itai s slide brochure www.cs.technion.ac.il/~dang/courseds Geiger & Itai, מיון בעזרת תור עדיפויות\ערימה - תור עדיפויות / ערימה - מיון בעזרת ערימה. אתחול: make_heap(). לכל בקלט: ) insert(,. כל עוד התור אינו ריק: Output max(); del_max() במימוש הנאיבי באמצעות עץ חיפוש: כל פעולת הכנסה, הוצאה, חיפוש בזמן (.(log סה"כ זמן ).( log בפעולות שהגדרנו, בניית ערימה מ- איברים יכולה לקחת ( Θ( log זמן כתלות בסדר ההכנסה. נגדיר פעולת ) make_heap(,,, היוצרת ערימה בת איברים (פעולה זו מחליפה את שני הצעדים הראשונים באלגוריתם המיון הרשום מעלה). נראה שמימוש יעיל של פעולה זו דורש () זמן. cs, Technion תור עדיפויות (או ערימה - (Heap הוא מבנה נתונים המוגדר ע"י הפעולות הבאות: צור ערימה ריקה. הכנס רשומה לערימה. הדפס את הרשומה עם המפתח הגדול ביותר בערימה. הוצא את הרשומה עם המפתח הגדול ביותר בערימה. MakeHeap() Insert(, ) Max() del_max() רשומה כל שלו: הנפוצים השימושים אחד לאור עדיפויות תור נקרא המבנה דרגת עדיפות. המשימה הבאה לבצוע היא המשימה (job) עם מגדירה משימה תור רגיל שבו עדיפות גבוהה מבנה זה מכליל בעלת העדיפות הגבוהה ביותר. ניתנת לפי סדר ההכנסה. מימוש נאיבי לערימה עץ חיפוש מאוזן. כל פעולת הכנסה והוצאה בזמן (.(log cs, Technion
פרוצדורת עזר (sift-down) בזמן הוצאה והכנסה יש לשמור על תכונת הערימה. לשם כך נשתמש בפרוצדורה ההופכת עץ בינרי כמעט שלם אשר עבורו מופרת תכונת הערימה רק בשורש בחזרה לערימה. העץ מורכב משורש המצביע לשתי ערימות: sift-down() כיצד נתקן את הערימה?. אם עלה, או גדול משני בניו סיים (העץ הנתון מהוה ערימה).. אחרת, החלף את השורש עם הבן בעל המפתח המקסימלי והמשך עם הערימה ששורשה. אלגוריתם זה נקרא,sift-down() "סינון כלפי מטה". cs, Technion מימוש ערימה מימוש ערימה בעזרת עץ בינרי כמעט שלם המפתח של הורה גדול או שווה ממפתחות ילדיו (תנאי זה נקרא תכונת הערימה). הסדר בין הילדים אינו מוגבל. דוגמא: כזכור מהרצאות קודמות, עץ כמעט שלם ניתן לייצג גם במערך: כאשר ההורה של צומת הוא הצומת /, בן שמאלי של צומת הוא הצומת ובן ימני הוא הצומת +. cs, Technion נכונות פרוצדורת make-heap נכונות: לאור סדר בחירת הצמתים בזמן שהאלגוריתם מבצע sift-down() הצומת כבר מצביע על עצים המקיימים את תכונת הערימה. לפיכך לאחר ביצוע. נוצרת ערימה ששורשה,sift-down() נימוק זה נכון לכל צומת, כולל השורש, ולפיכך בסיום מוחזרת ערימה. פרוצדורת make-heap מימוש ) :make_heap(,,, עבור על כל צמתי העץ הפנימיים כך שנעבור על הבנים לפני הוריהם (למשל בסדר.(Postorder לכל צומת בעץ בצע.sift_down() דוגמא: זמן הריצה של make-heap נתון ע"י (סכום הגבהים של כל הצמתים), שכן לכל צומת מבצעים (h()) תיקונים כאשר h() הוא גובה הצומת. ניתוח פשוט של סכום הגבהים נותן חסם גס של (,( log שכן הגובה של כל צומת חסום ע"י (.(log ניתוח מדויק יותר ייקח בחשבון שלרוב הצמתים גובה קטן. ניתוח כזה נותן חסם הדוק של.() cs, Technion
.(log ) זמן:.sift_down() (,, ){ make_heap(,,, ); While ( ) { Output max(); del_max(); כיצד ממומש?max() הדפס את השורש. זמן: (). כיצד ממומש?del_max() שים עלה אחרון במקום השורש. בצע del_max Sift_down(r) cs, Technion דוגמא: המשופר, עם שימוש ב :Make_Heap ניתוח זמנים של פרוצדורת make-heap זמן הריצה של make-heap נתון ע"י סכום הגבהים של כל הצמתים:. Σ h הוכחת המשפט: ישנו צומת בודד בגובה h, צמתים בגובה h, בגובה,h ובאופן כללי צמתים בגובה.h לפיכך סכום הגבהים (שנסמנו ב- ) מקיים: גובה h צמתים מס' צמתים h h = h = = h + + + + + משפט: עבור עץ בינרי שלם בגובה h הכולל. צמתים, סכום הגבהים קטן מ- = מסקנה: זמן הריצה של Make_Heap על איברים הוא.() h + h + h + + = h + h + h + + ע"י חיסור המשוואה הראשונה מהשניה: = h + < cs, Technion אנליזת - מקום וזמן דוגמא למיון סיבוכיות זמן: סך הזמן הנדרש מורכב מזמן בניית הערימה () בתוספת פעולות. del_max כלומר הזמן הכללי הוא: () + ( log ) = ( log ) סיבוכיות מקום: פרוצדורת מיון זו פועלת ללא הזדקקות למקום נוסף מעבר למערך המקורי (פרט לתוספת () למשתנים זמניים). המקום הנדרש הוא.() cs, Technion המערך הנתון (כמו בשקף ): המערך לאחר make_heap (כמו בשקף :( פעולות המיון: del sift del sift del sift del sift del sift המערך הממוין: cs, Technion
(המשך) המיון תוכנית מימוש sift_down לערימת מקסימום heap_sort(n){ for (int i = n/; i > ; i--) sift_down(i,n)); for (int i = n; i > ; i--){ swap(,i); sift_down(,i-); /* בנית הערימה */ /* המיון עצמו */ הערה: בתוכנית זו הפלט ממוין מהקטן לגדול כמודגם בשקפים ו-. יתרונות אלגוריתם למיון: זמן במקרה הגרוע ).( log לא דורש מקום עזר. sift_down(first, last) { for (int r = first; r <= last/; ){ if (*r = = last){ /* r has one child at *r */ if (a[r] < a[*r]) swap(r,*r); else { /* r has two children at *r and *r+ */ if (a[r] < a[*r] && a[*r] >= a[*r+]) { swap(a+r, a+*r); r *= ; else if (a[r] < a[*r + ] && a[*r + ] >= a[*r]) { swap(a+r, a+*r + ); r = *r + ; else break; cs, Technion cs, Technion מימוש sift_down לערימת מינימום ערימת מינימום sift_down(first, last) { for (int r = first; r <= last/; ){ if (r = = last){ /* r has one child at *r */ if (a[r] > a[*r]) swap(r,*r); else { /* r has two children at *r and *r+ */ if (a[r] > a[*r] && a[*r] <= a[*r+]){ swap(a+r, a+*r); r *= ; else if (a[r] > a[*r + ] && a[*r + ] <= a[*r]){ swap(a+r, a+*r + ); r *= *r + ; else break; cs, Technion תור עדיפויות מוגדר לעתים להיות ערימת מינימום, המוגדרת ע"י הפעולות: מימוש ערימת מינימום: באופן סימטרי למימוש ערימת מקסימום: בעזרת עץ בינרי כמעט שלם כאשר המפתח של הורה קטן או שווה ממפתחות ילדיו. (,, ){ make_heap(,,, ); While ( ) { Output min(); del_min(); MakeHeap(,,, ) אלגוריתם עם ערימת מינימום: צור ערימה המכילה את הרשומות.,, הכנס רשומה לערימה. הדפס את הרשומה עם המפתח הקטן ביותר בערימה. הוצא את הרשומה עם המפתח הקטן ביותר בערימה. Insert(, ) Min() del_min() cs, Technion
(המשך) מיון (Key *A, int l, int r){ if (l >= r) return ; int p = choose_pivot(a,l,r); int i = partition(a,l,r,p); (A, l, i-); (A,i+,r); נכונות בקצרה: איבר הציר נמצא במקום המתאים לו במערך אחרי ביצוע.Partition הקריאות הרקורסיביות ממיינות את האיברים שאחרי איבר הציר ולפני איבר הציר. < < < זמן המיון - תלוי באיבר הציר. נראה בהמשך:. במקרה הגרוע ביותר נקבל זמן ).Θ(. אם בכל שלב המערך נחצה בצורה קרובה לשווה, אז נקבל זמן (.( log. אם איבר הציר נבחר באקראי, נקבל זמן ריצה של ( ( log בממוצע. כלומר האטרקטיביות של אלגוריתם זה אינה נובעת מזמן הריצה המקסימלי. cs, Technion המיון משתמש בשיטת הפרד ומשול:.,, בכל שלב ממיינים את המערך נבחר איבר כלשהו מתוך [ [,, שיקרא איבר הציר (pivot) ויסומן ב-[ A[. = או נבחר את באקראי. למשל נבחר הפרד: סדר את [ [,, כך שכל האיברים הקטנים מאיבר הציר ימצאו בתאים ימצאו לו שווים או הגדולים האיברים וכל [ [,, בתאים.[] כאשר איבר הציר נמצא במקום,[,, [ ].[ +,, משול: מיין רקורסיבית את המערכים ] [,, ואת (Key *A, int l, int r){ if (l >= r) return ; int p = choose_pivot(a,l,r); int i = partition(a,l,r,p); (A, l, i-); (A,i+,r); הפרד: משול: השגרה partition מחזירה את האינדקס בו שמור איבר הציר אחרי הפעלתה. cs, Technion ניתוח זמנים במקרה הגרוע פרוצדורת - ההפרדה לפי איבר ציר Partition נתון מערך ממוין מהקטן לגדול. נניח שאיבר הציר נבחר להיות האיבר הקטן ביותר. בכל קריאה רקורסיבית המערך [ [, מתבצעת קריאה רקורסיבית למיון המערך.[ +, ] והמערך [, ] לשם המחשה נצייר את עץ הקריאות הרקורסיביות: n n n n- n n- n- n n int partition( KEY *A, int left, int right, int pivot){ swap (A+pivot, A+right); int i = left; for (int j=left; j < right; j++) if (A[j] < A[right]) { swap(a+i, A+j); i++; swap(a+right, A+i); return i; דוגמא:.pivot=left איבר הציר הוא המספר. זמן הריצה: = = + = + + = = Θ( ) cs, Technion העברת איבר הציר. זמן ריצה:.Θ() cs, Technion i
ניתוח מקום ניתוח זמנים n n n המקום שדרוש ל כולל: מערך הקלט. משתנים מקומיים (). מחסנית הרקורסיה. במקרה הגרוע עומק הרקורסיה הוא.() n- n- n- n n n זמן הריצה תלוי באיבר הציר. מקרה אופטימלי. איבר הציר הוא חציון. חלוקה מאוזנת. איבר הציר מחלק את המערך כך שכל חלק יכיל אחוז מסוים מגודל המערך (למשל %). נראה כעת שיטה להגבלת עומק הרקורסיה ל- ( (log במקרה הגרוע. = (/) + Θ() = Θ( log ) = (/) + (/) + Θ() = Θ( log ) מקרה ממוצע. איבר הציר הוא אקראי. = Θ + + ( ) on average = Θ( log ) on average cs, Technion עץ הקריאות הרקורסיביות הוא עץ בינרי ממוצע. נוסחת הנסיגה זהה לנוסחת הנסיגה לזמן בניית עץ חיפוש בינרי אקראי ומכאן זמן הריצה של Quicksort הוא ) ( log בממוצע. cs, Technion (Key *a, int l, int r) { while (l < r){ int p = choose_pivot(a,l,r); int i = partition(a,l,r,p); (a, l, i-); l = i + ; העלמת רקורסיית זנב Tail recursion elimination....-.. החלפת הקריאה הרקורסיבית השניה באיטרציה: בקומפיילרים מודרניים מימוש של העלמת רקורסיית זנב נעשה אוטומטית. cs, Technion........................ דוגמא להתנהגות מחסנית הרקורסיה (Key *a, int l, int r) { if (l >= r) return ; int p = choose_pivot(a,l,r); int i = partition(a,l,r,p); (a, l, i-); (a, i+, r);......-........................ בכל צומת מצוין טווח האינדקסים ולצד הצומת מצוין עומק המחסנית. cs, Technion
סיכום השיעור העלמת רקורסיה לפי גודלה בשיעור זה ראינו: ערימה(תור עדיפויות) מבנה נתונים חדש ומימוש של מבנה זה..( log ( שימוש בערימה למטרת מיון ללא זיכרון נוסף בזמן - Heapsort Quicksort והוכחת זמן ריצה ( ( log בממוצע. העלמת רקורסיה שיטה להקטנת עומק מחסנית הרקורסיה עבור תוכניות רקורסיביות. cs, Technion.... (Key *a, int l, int r) { while (l < r){ int p = choose_pivot(a,l,r); int i = partition(a,l,r,p); if (i - l < r i) { (a, l, i-); l = i + ; else { (a, i+, r); r = i - ;..-.... החלפת הקריאה הרקורסיבית הגדולה באיטרציה. כלומר, קריאה רקורסיבית רק על חלק המערך הקטן. התנהגות המחסנית על הדוגמא הקודמת (בבחירה זהה של איבר הציר): במימוש זה עומק הרקורסיה חסום ע"י () log כיוון שבכל קריאה רקורסיבית המערך קטן בלפחות חצי. cs, Technion........................ cs, Technion